/*************************************************************************
 * Copyright 2008 David Almilli
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *      
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 **************************************************************************/
package com.tivo.hme.bananas;

import java.awt.Rectangle;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import com.tivo.hme.interfaces.IContext;
import com.tivo.hme.interfaces.IHmeConstants;
import com.tivo.hme.interfaces.ILogger;
import com.tivo.hme.sdk.FactoryPlus;
import com.tivo.hme.sdk.HmeEvent;
import com.tivo.hme.sdk.HmeException;
import com.tivo.hme.sdk.Resolution;
import com.tivo.hme.sdk.ResolutionInfo;
import com.tivo.hme.sdk.View;
import com.tivo.hme.sdk.io.ChunkedInputStream;
import com.tivo.hme.sdk.io.ChunkedOutputStream;
import com.tivo.hme.sdk.util.Ticker;

/**
 * The <code>BApplicationPlus</code> class extends BApplication to automatically change to the
 * preferred rendering resolution.  It also configures the application skin to use the appropriate
 * skin for that resolution.  After the resolution change happens, it will call the init() method.
 * @author s2kdave
 */
public class BApplicationPlus extends BApplication {
    
    public static final int CMD_RECEIVER_SET_RESOLUTION = 62;
    
    private static final Field stackField;
    static {
    	Field f = null;
    	try {
			f = BApplication.class.getDeclaredField("stack");
			f.setAccessible(true);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException("Cannot find stack field in BApplication class");
		}
    	stackField = f;
    }

    private ChunkedOutputStream output;
    private int protocolVersion;
    private ResolutionInfo resolutionInfo;
    private int width;
    private int height;
    private Resolution desiredResolution;
    protected boolean initialized;
    private boolean resolutionReceived;
    private boolean applicationInitialized;
    private Timer timer;
    private Vector<StackFrame> stack;

    /**
     * This will initialize the context by configuring the current resolution and application
     * bounds.
     */
    protected void setContext(IContext context, int version) {
        log(ILogger.LOG_DEBUG, "initContext version="+version);
        this.protocolVersion = version;
        
        resolutionInfo = createResolutionInfo(640, 480, 1, 1);

        if (context.getOutputStream() instanceof ChunkedOutputStream) {
            this.output = (ChunkedOutputStream)context.getOutputStream();
        } else {
            this.output = new ChunkedOutputStream(context.getOutputStream(), IHmeConstants.TCP_BUFFER_SIZE);
        }
        
        super.setContext(context, version);

        this.width = getRoot().getWidth();
        this.height = getRoot().getHeight();
    }
    
    /**
     * Initializes the the HDApplication with the context.  Do not override this method unless
     * you know what you are doing.  The resolution information may or may not be available
     * by the time this method is called.
     */
    @SuppressWarnings("unchecked")
    public void init(IContext context) throws Exception {
        super.init(context);
    	stack = (Vector<StackFrame>)stackField.get(this);
        setSkin(new BSkinPlus(this));
        
        log(ILogger.LOG_DEBUG, "init context resolutionReceived="+resolutionReceived);
        
        if (resolutionReceived) {
            initApp();
        }
    }
    
    /**
     * Adds support for handling the resolution change events
     * @param in the input stream the read from
     * @return true if the application should handle more
     */
    public boolean handleChunk(InputStream in) {
        boolean doMore = true;

        // flush any data that was generated from previous handling of
        // event
        flush();

        ChunkedInputStream chunkInStr = null;
        if (in instanceof ChunkedInputStream) {
            chunkInStr = (ChunkedInputStream) in;
        } else {
            chunkInStr = new ChunkedInputStream(in); // , IHmeConstants.TCP_BUFFER_SIZE);
        }

        if (protocolVersion < VERSION_0_40) {
            chunkInStr.setUseVString(false);
        } else {
            chunkInStr.setUseVString(true);
        }

        int opcode = -1;
        try {
            opcode = (int) chunkInStr.readVInt();
        } catch (IOException e) {
            // receiver closed - ignore
        }

        if (opcode == -1) {
            doMore = false;
            return doMore;
        }

        HmeEvent evt = null;
        try {
            switch (opcode) {
            case EVT_DEVICE_INFO:
                evt = new HmeEvent.DeviceInfo(chunkInStr);
                break;
            case EVT_APP_INFO:
                evt = new HmeEvent.ApplicationInfo(chunkInStr);
                initApp();
                break;

            case EVT_RSRC_INFO:
                evt = new HmeEvent.ResourceInfo(chunkInStr, this);
                break;
            case EVT_KEY:
                evt = new HmeEvent.Key(chunkInStr);
                break;
            case EVT_IDLE:
                evt = new HmeEvent.Idle(chunkInStr);
                break;
            case EVT_FONT_INFO:
                evt = new HmeEvent.FontInfo(chunkInStr);
                initApp();
                break;

            case EVT_INIT_INFO:
                evt = new HmeEvent.InitInfo(chunkInStr);
                initApp();
                break;

            case ResolutionInfo.EVT_RES_INFO:
                log(ILogger.LOG_DEBUG, "handleChunk: ResolutionInfo");
                this.resolutionInfo = new ResolutionInfo(chunkInStr);
                evt = resolutionInfo;
                Resolution res = resolutionInfo.getCurrentResolution();
                if (res.getWidth() != getRoot().getWidth()
                        || res.getHeight() != getRoot().getHeight()) {
                    getRoot().setSize(res.getWidth(), res.getHeight());
                    width = getRoot().getWidth();
                    height = getRoot().getHeight();
                    rootBoundsChanged(getRoot().getBounds());
                }
                resolutionReceived = true;
                initApp();
                break;
            }
            chunkInStr.readTerminator();
        } catch (IOException e) {
            evt = null;
            e.printStackTrace();
        }

        if (evt == null) {
            log(ILogger.LOG_DEBUG, "unknown event opcode : " + opcode);
        } else {
            dispatchEvent(evt);
        }
        // flush any data that was generated from handling of event
        flush();
        return doMore;
    }

    public void pushAndFinish(BScreen screen, int transition) {
    	pushAndFinish(screen, transition, null);
    }
    
    @Override
    public void pop(Object arg) {
    	BScreen toPop = getCurrentScreen();
    	super.pop(arg);
    	
    	if (toPop instanceof BScreenPlus) {
    		((BScreenPlus)toPop).handleFinish();
    	}
    }
    public void pushAndFinish(BScreen screen, int transition, Object arg) {
    	super.push(screen, transition, arg);
    	finish(screen);
    }
    
    public boolean isOnStack(BScreen screen) {
    	return getStackFrame(screen) != null;
    }
    
    private StackFrame getStackFrame(BScreen screen) {
    	StackFrame screenFrame = null;
    	Iterator<StackFrame> iter = stack.iterator();
    	while (iter.hasNext()) {
    		StackFrame frame = iter.next();
    		if (frame.screen == screen) {
    			screenFrame = frame;
    			break;
    		}
    	}
    	return screenFrame;
    }
    
    public void finish(final BScreen screen) {
    	Ticker.master.add(new Ticker.Client() {
			public long tick(long tm, Object arg) {
		    	BScreen current = getCurrentScreen();
		    	if (current == screen) {
		    		//just pop it since it's the current screen
		    		pop();
		    		return 0;
		    	}

		        getRoot().setPainting(false);
		        try {
			    	StackFrame screenFrame = getStackFrame(screen);
			    	if (screenFrame != null) {
				    	//it's not the current screen so just remove it from the stack
			    		stack.remove(screenFrame);
			
						if (screenFrame.transition == TRANSITION_LEFT) {
					    	//find the index where the child is in the normal so we can untranslate the remaining
					    	BView normal = getNormal();
							int index = -1;
					    	for (int i=0; index == -1 && i < normal.getChildCount(); i++) {
					    		if (normal.getChild(i) == screen) {
					    			index = i;
					    		}
					    	}
					    	
							int width = normal.getWidth();
					    	if (index != -1) {
								//adjust the children after so that they are in the proper place in the normal buffer
								for (int i=index+1; i < normal.getChildCount(); i++) {
									View view = normal.getChild(i);
									view.setLocation(view.getX()-width, view.getY());
								}
								normal.translate(width, 0);
					    	}
						}
			    	}
					screen.setVisible(false);
		        } finally {
		            getRoot().setPainting(true);
		        }
		    	
		    	if (screen instanceof BScreenPlus) {
		    		((BScreenPlus)screen).handleFinish();
		    	}
				return 0;
			}
    	}, System.currentTimeMillis()+500, null);
        	
    }

    /**
     * Sends a request to change the receiver's resolution.  This is an asynchronous call
     * and may or may not change the resolution.  
     * @param res the requested resolution
     */
    public void setReceiverResolution(Resolution res) {
        try {
            if (timer == null) {
                timer = new Timer("Resolution Changer", true);
            }
            timer.schedule(new TimerTask() {
                public void run() {
                    if (!resolutionReceived) {
                        log(ILogger.LOG_WARNING, "Timeout waiting for resolution change.");
                    }
                }
            }, 30000);
            
            // request the resolution change
            log(ILogger.LOG_DEBUG, "Resolution change requested: " + res);
            resolutionReceived = false;
            cmdReceiverSetResolution(res.getWidth(), res.getHeight(),
                    res.getPixelAspectNumerator(), res.getPixelAspectDenominator());
            
        } catch (Exception e) {
            log(ILogger.LOG_DEBUG, "Error changing receiver resolution: " + e.getMessage());
        }
    }


    void cmdReceiverSetResolution(int width, int height, int aspectNumerator, int aspectDenominator) {
        try {
            output.writeVInt(CMD_RECEIVER_SET_RESOLUTION);
            output.writeVInt(1);
            
            output.writeVInt(width);
            output.writeVInt(height);
            output.writeVInt(aspectNumerator);
            output.writeVInt(aspectDenominator);
            
            output.writeTerminator();
            output.flush();
        } catch (Throwable t) {
            throw new HmeException(t);
        }
    }

    /**
     * Gets the current resolution information about the receiver that this application is
     * running against.
     * @return the TiVo receiver resolution information
     */
    public ResolutionInfo getResolutionInfo() {
        return resolutionInfo;
    }

    public Resolution getCurrentResolution() {
        return resolutionInfo.getCurrentResolution();
    }
    
    /**
     * Gets the width of the application
     * @return the application width
     */
    public int getWidth() {
        return width;
    }
    
    /**
     * Gets the height of the application
     * @return the application height
     */
    public int getHeight() {
        return height;
    }

    ResolutionInfo createResolutionInfo(int width, int height, int aspectNumerator, int aspectDenominator) {
        Resolution resolution = new Resolution(width, height, aspectNumerator, aspectDenominator);
        List<Resolution> supported = new ArrayList<Resolution>(1);
        supported.add(resolution);
        return new ResolutionInfo(resolution, supported);
    }
    
    private void initApp() {
        if (applicationInitialized) {
            log(ILogger.LOG_DEBUG, "Already initialized, not re-initing.");
            return;
        }
        applicationInitialized = true;
        
        try {
            initSkin();
            log(ILogger.LOG_DEBUG, "calling initResolution()");
            initResolution();
        } catch (Throwable e) {
            log(ILogger.LOG_WARNING, "An error occurred during application initialization.", e);
        }
    }

    /**
     * Gets the safe action horizontal inset from the edge of the left/right edges.
     * @return the safe action horizontal inset
     */
    public int getSafeActionHorizontal() {
        return resolutionInfo.getCurrentResolution().getSafeActionHorizontal();
    }

    /**
     * Gets the safe action vertical inset from the edge of the top/bottom edges.
     * @return the safe action vertical inset
     */
    public int getSafeActionVertical() {
        return resolutionInfo.getCurrentResolution().getSafeActionVertical();
    }

    /**
     * Gets the safe title horizontal inset from the edge of the left/right edges.
     * @return the safe title horizontal inset
     */
    public int getSafeTitleHorizontal() {
        return resolutionInfo.getCurrentResolution().getSafeTitleHorizontal();
    }

    /**
     * Gets the safe title vertical inset from the edge of the top/bottom edges.
     * @return the safe title vertical inset
     */
    public int getSafeTitleVertical() {
        return resolutionInfo.getCurrentResolution().getSafeTitleVertical();
    }
    
    /**
     * Gets called when the root bounds changes.  This will change the bounds of the below,
     * normal, and above view layers and then reinitialize the skin.
     * @param r the new bounds of the root application
     */
    public void rootBoundsChanged(Rectangle r) {
        log(ILogger.LOG_DEBUG, "rootBoundsChanged r="+r);
        getBelow().setBounds(r.x, r.y, r.width, r.height);
        getNormal().setBounds(r.x, r.y, r.width, r.height);
        getAbove().setBounds(r.x, r.y, r.width, r.height);
        initSkin();
    }


    protected void initSkin() {
        getSkin().configure(getCurrentResolution());
    }
    
    /**
     * Provides the default functionality of changing to the preferred resolution.  If you
     * want, you can override this method to change the functionality of what happens
     * on initialization.
     * @param context the context for the application
     * @throws Exception
     */
    protected void initResolution() throws Exception {
        // Check the current resolution. If needed, tell receiver to switch to
        // the desired resolution and return. When the receiver finally gets to
        // the desired resolution, call the application's init method.
        ResolutionInfo resInfo = getResolutionInfo();
        Resolution currentRes = resInfo.getCurrentResolution();
        desiredResolution = resInfo.getPreferredResolution();

        log(ILogger.LOG_INFO, "Current resolution is: " + currentRes);

        if (currentRes.equals(desiredResolution)) {
            if (!initialized) {
                init();
                initialized = true;
            }
        } else {
            log(ILogger.LOG_INFO, "Changing resolution to: " + desiredResolution);
            // switch the the preferred resolution
            setReceiverResolution(desiredResolution);
        }
    }

    /**
     * This event handler snoops on resolution changes and reinitializes the
     * service when it sees one.
     */
    public boolean handleEvent(HmeEvent event) {
        if (event instanceof ResolutionInfo) {
            ResolutionInfo resInfo = (ResolutionInfo) event;

            log(ILogger.LOG_INFO, "Received resolution event: " + resInfo);
            if (!initialized && desiredResolution != null
                && resInfo.getCurrentResolution().equals(desiredResolution)) {
                init();
            }
        }
        return super.handleEvent(event);
    }

    /**
     * This init method gets called after the resolution has been changed to the 
     * preferred resolution.
     */
    protected void init() {}

    public void log(int priority, Throwable t) {
        super.log(priority, FactoryPlus.getStackTrace(t));
    }

    public void log(int priority, String message, Throwable t) {
        super.log(priority, message + "\n" + FactoryPlus.getStackTrace(t));
    }


    /**
     * Selects the preferred resolution for this application given the list of supported
     * resolutions. You may override this if you want to select a different resolution other than
     * calling resInfo.getPreferredResolution().
     * 
     * @param resInfo
     *            contains all the supported resolutions.
     * @return the desired resolution.
     */
    protected Resolution getPreferredResolution(ResolutionInfo resInfo) {
        return resInfo.getPreferredResolution();
    }

    public BSkinPlus getSkin() {
        return (BSkinPlus) super.getSkin();
    }

    @Override
    public void setSkin(BSkin skin) {
        if (!(skin instanceof BSkinPlus)) {
            throw new IllegalArgumentException("You must use a BSkinPlus with this application.");
        }
        super.setSkin(skin);
    }
}
